Introduzione¶
L'obiettivo di questo progetto è quello di applicare tecniche di analisi dati adottando un approccio data-driven per il lancio di una ipotetica nuova applicazione mobile.
Per realizzare questa analisi, vi sono a disposizione due fonti di dati. La prima è un dataset completo delle applicazioni presenti sul Play Store che fornisce informazioni dettagliate su ogni app: dai rating alle recensioni, dal numero di installazioni al prezzo, fino alle caratteristiche tecniche. La seconda fonte è una raccolta di recensioni degli utenti, già elaborate con tecniche di sentiment analysis.
Partendo dalle fondamenta, ho cercato di creare una struttura robusta per la gestione dei dati, con particolare attenzione alla pulizia e alla preparazione degli stessi.
Un aspetto particolarmente interessante dell'analisi sarà lo studio della competizione nelle diverse categorie. Alcune categorie potrebbero sembrare attraenti a prima vista, magari per l'alto numero di download, ma potrebbero rivelarsi mercati saturi con forte competizione. Altre categorie, apparentemente più piccole, potrebbero nascondere nicchie interessanti con meno concorrenza e, potenzialmente, utenti più disposti a pagare per un prodotto di qualità.
Il codice è stato strutturato seguendo un approccio modulare, con particolare attenzione alla chiarezza e alla documentazione. Ogni fase dell'analisi è organizzata in componenti logiche ben definite, permettendo di seguire facilmente il processo di analisi dai dati grezzi fino alle conclusioni finali. Questa struttura non solo dovrebbe rendere il codice più robusto, ma facilita anche la verifica e la validazione dei risultati ottenuti in ogni fase dell'analisi.
1. Import e setup¶
La seguente cella di codice ha lo scopo di preparare l'ambiente di lavoro per l'analisi dei dati. Si occupa quindi dell'importazione delle librerie e della configurazione dell'ambiente di analisi.
Le librerie utilizzate sono le seguenti:
warning: per gestire e disabilitare i messaggi di avvertimento che potrebbero disturbare l'output dell'analisi;logging: per tracciare le varie fasi dell'analisi e catturare eventuali errori o comportamenti inaspettati durante l'elaborazione dei dati;typing: permette di specificare i tipi di dati attesi per ogni variabile e funzione, rendendo il codice più robusto e auto-documentato. Nel mio caso l'ho utilizzato per definire chiaramente cosa aspettarsi da input e output delle funzioni, ad esempio conDictper i dizionari oOptionalper valori che potrebbero essere None;dataclasses: semplifica la creazione di classi destinate a contenere dati;pathlibeos: librerie che lavorano insieme per gestire le operazioni sui file, come la verifica dell'esistenza dei CSV e la gestione dei percorsi, indipendentemente dal sistema operativo utilizzato;lru_cachedafunctools: implementa una memoria cache per le funzioni di formattazione più utilizzate, evitando di ricalcolare risultati già ottenuti e migliorando le performance;ThreadPoolExecutordaconcurrent.futures: permette di parallelizzare alcune operazioni di elaborazione dati particolarmente pesanti, migliorando le performance dell'analisi;re: per le operazioni di pulizia e standardizzazione dei dati, in particolare per estrarre informazioni numeriche da stringhe come prezzi e dimensioni delle app;datetime: necessario per l'analisi temporale dei dati, in particolare per calcolare intervalli di tempo;pandas: per creare e manipolare dataframe;numpy: complementare a pandas, fornisce le funzionalità per calcoli numerici avanzati, come il calcolo di metriche, statistiche e la gestione di array multidimensionali necessari per l'analisi delle performance delle app;pandas.api.types: per implementare controlli più rigidi sui tipi di dati nelle colonne dei dataframe, assicurando che le operazioni numeriche vengano eseguite solo su dati appropriati.
Per la visualizzazione dei dati, ho scelto di utilizzare due approcci complementari:
plotly(con i suoi moduli express,graph_objectsesubplots) per creare visualizzazioni interattive e dettagliate delle metriche del Play Store, permettendo un'esplorazione dinamica dei dati;matplotlib.pyploteseabornper generare visualizzazioni statistiche più tradizionali, particolarmente utili per l'analisi delle distribuzioni e delle correlazioni.
I warning vengono disabilitati con warnings.filterwarnings('ignore') per mantenere l'output pulito, mentre il sistema di logging viene configurato attraverso logging.basicConfig() per tracciare operazioni ed errori.
La classe PlotConfig, definita usando il decoratore @dataclass, centralizza le configurazioni per la visualizzazione.
COLOR_PALETTE mappa stati come 'primary', 'success', 'warning' ai rispettivi codici colore esadecimali, mentre PLOT_STYLE definisce uno stile uniforme per i grafici con font, dimensioni e caratteristiche di base.
l metodo __post_init__ viene chiamato automaticamente dopo il metodo __init__ e viene usato per definire i valori di default di COLOR_PALETTE e PLOT_STYLE.
Ho poi implementato la classe DataFormatter per gestire la formattazione dei dati. Contiene tre metodi statici, ciascuno decorato con @lru_cache(maxsize=1000) per la memorizzazione dei risultati: format_number(), format_percentage() e format_currency(), che gestiscono rispettivamente numeri con separatori di migliaia, percentuali con un decimale e valori monetari.
Il decoratore @staticmethod indica appunto che questi sono metodi statici, il che significa che possono essere chiamati direttamente sulla classe senza creare un'istanza della stessa.
La classe DataLoader costituisce il nucleo del caricamento dati. Il metodo _is_colab_environment() verifica l'esecuzione su Google Colab, mentre _setup_visualization_settings() configura le impostazioni di pandas e degli strumenti di visualizzazione.
Il metodo principale load_data() gestisce il caricamento dei file CSV attraverso un blocco try-except, registrando eventuali errori tramite il logger.
L'inizializzazione avviene con data_loader = DataLoader(), seguito dal tentativo di caricamento dei dati. Un controllo con if apps_df is None or reviews_df is None verifica il successo dell'operazione, terminando l'esecuzione con sys.exit(1) in caso di errore.
Per quanto riguarda le librerie di visualizzazione, plotly.express e plotly.graph_objects permetteranno di creare grafici interattivi, mentre matplotlib.pyplot e seaborn saranno utilizzati per visualizzazioni statistiche tradizionali.
La manipolazione e l'analisi dei dati numerici si baseranno su numpy e pandas.
# Gestione warning e logging
import warnings
import logging
from typing import Dict, List, Tuple, Optional, Any, NamedTuple, Union
from dataclasses import dataclass, field
from pathlib import Path
import sys
import os
from functools import lru_cache
from concurrent.futures import ThreadPoolExecutor
import re
from datetime import datetime
# Disabilita warnings
warnings.filterwarnings('ignore')
# Setup logging base
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Librerie di base per l'analisi dati
import pandas as pd
import numpy as np
from pandas.api.types import is_numeric_dtype
# Librerie per visualizzazione
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import matplotlib.pyplot as plt
import seaborn as sns
@dataclass
class PlotConfig:
COLOR_PALETTE: Dict[str, str] = None
PLOT_STYLE: Dict[str, Any] = None
def __post_init__(self):
self.COLOR_PALETTE = {
'primary': '#2c3e50',
'secondary': '#34495e',
'success': '#27ae60',
'warning': '#f39c12',
'danger': '#c0392b',
'info': '#3498db'
}
self.PLOT_STYLE = {
'template': 'plotly_white',
'font_family': 'Arial, sans-serif',
'title_font_size': 20,
'title_x': 0.5,
'showlegend': True
}
class DataFormatter:
@staticmethod
@lru_cache(maxsize=1000)
def format_number(num: Union[int, float]) -> str:
"""Formatta numeri con separatori di migliaia"""
return f"{num:,.0f}"
@staticmethod
@lru_cache(maxsize=1000)
def format_percentage(num: Union[int, float]) -> str:
"""Formatta percentuali con 1 decimale"""
return f"{num:.1f}%"
@staticmethod
@lru_cache(maxsize=1000)
def format_currency(num: Union[int, float]) -> str:
"""Formatta valori monetari"""
return f"${num:,.2f}"
class DataLoader:
def __init__(self):
self.plot_config = PlotConfig()
def _is_colab_environment(self) -> bool:
try:
import google.colab
return True
except ImportError:
return False
def _setup_visualization_settings(self):
# Impostazioni pandas
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 100)
pd.set_option('display.float_format', lambda x: '%.3f' % x)
# Impostazioni matplotlib/seaborn
plt.style.use('default')
sns.set_theme(style='whitegrid')
def load_data(self) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
try:
# Setup visualizzazione
self._setup_visualization_settings()
# Determina l' ambiente e carica i dati
if self._is_colab_environment():
logger.info("Ambiente rilevato: Google Colab")
from google.colab import files
logger.info("Per favore, carica i file 'googleplaystore.csv' e 'googleplaystore_user_reviews.csv'")
uploaded = files.upload()
# Verifica presenza file
required_files = ['googleplaystore.csv', 'googleplaystore_user_reviews.csv']
for file in required_files:
if not os.path.exists(file):
raise FileNotFoundError(f"File {file} non trovato nella directory corrente")
apps_df = pd.read_csv('googleplaystore.csv')
reviews_df = pd.read_csv('googleplaystore_user_reviews.csv')
logger.info(f"Dataset caricati con successo!")
logger.info(f"Dimensioni apps_df: {apps_df.shape}")
logger.info(f"Dimensioni reviews_df: {reviews_df.shape}")
return apps_df, reviews_df
except Exception as e:
logger.error(f"Errore nel caricamento dei dati: {str(e)}")
return None, None
# Inizializzazione del loader e caricamento dei dati
data_loader = DataLoader()
apps_df, reviews_df = data_loader.load_data()
# Verifica che i dati siano stati caricati correttamente
if apps_df is None or reviews_df is None:
logger.error("Errore nel caricamento dei dataset. Verifica la presenza dei file o ricaricali.")
sys.exit(1)
Saving googleplaystore_user_reviews.csv to googleplaystore_user_reviews.csv Saving googleplaystore.csv to googleplaystore.csv
2. Lettura e validazione dei dati¶
Il secondo blocco del codice si occupa della validazione e dell'analisi iniziale dei due dataset principali: apps_df, che contiene le informazioni sulle app del Google Play Store, e reviews_df, che contiene le recensioni degli utenti.
All'inizio della cella importo partial da functools, che utilizzerò per la parallelizzazione delle operazioni di validazione.
Vado a definire poi una serie di classi specializzate per gestire diversi aspetti della validazione dei dati.
La classe DatasetMetrics è definita come @dataclass e funge da contenitore per le metriche principali di un dataset. Include campi come:
rowsecolumnsper le dimensioni;missing_dataper tracciare le percentuali di valori mancanti per colonna;duplicateseduplicate_percentageper i record duplicati;dtypesper i tipi di dati delle colonne;unique_countsper contare i valori unici in ogni colonna.
La classe DataValidator implementa la logica di validazione vera e propria. Nel costruttore accetta max_workers per controllare la parallelizzazione.
Il metodo _calculate_column_metrics è anch'esso decorato con @staticmethod perché non necessita di accedere allo stato dell'istanza della classe. Prende in input un dataframe (df) e il nome di una colonna (column) e restituisce un dizionario con le seguenti metriche:
tipo: il tipo di dati della colonna ottenuto condtypee convertito in stringa;non_nulli: il numero di valori non nulli calcolato con count();nulli: il numero di valori nulli ottenuto conisnull().sum()perc_nulli: la percentuale di valori nulli calcolata come(nulli/totale)*100e arrotondata a 2 decimaliunique_values: il numero di valori unici nella colonna ottenuto connunique().
Il metodo validate_dataset è il cuore della validazione e utilizza ThreadPoolExecutor per parallelizzare i calcoli sulle colonne. Attraverso executor.map e partial, applica _calculate_column_metrics a tutte le colonne contemporaneamente. Calcola anche il numero di duplicati nel dataset usando duplicated().sum().
Ho poi implementato la classe DataConsistencyChecker per verificare la coerenza tra i due dataset. Il suo metodo check_consistency (decorato con @staticmethod) usa operazioni su insiemi per confrontare le app presenti nei dataset:
- crea due set con
unique()per ottenere le app uniche in ciascun dataset; - usa
intersectionper trovare le app presenti in entrambi; - usa l'operatore
-per identificare le app con recensioni ma assenti nel dataset principale.
Durante l'esecuzione, vengono registrate nel log diverse statistiche utili sulla distribuzione delle app tra i dataset. Il metodo tiene traccia del numero totale di app in ciascun dataset, di quante sono in comune e genera un warning se trova app che hanno recensioni ma non esistono nel dataset principale. Viene anche verificata l'integrità complessiva dei dati, registrando il numero di app uniche nel Play Store e il totale dei record nelle recensioni. Al termine della verifica, il metodo restituisce i dataframe originali in una tupla, senza apportare modifiche.
La classe InitialAnalyzer fornisce una prima panoramica dei dati. Il suo metodo analyze_dataset separa le colonne in numeriche e categoriche usando select_dtypes:
- per le colonne numeriche usa
describe()per ottenere statistiche di base; - per le colonne categoriche conta i valori unici e, se sono meno di 10, calcola la distribuzione con
value_counts(normalize=True).
La funzione principale validate_and_analyze_data di fatto gestisce tutto il processo:
- inizializza le classi necessarie se non fornite;
- esegue la validazione di entrambi i dataset;
- verifica la loro consistenza;
- conduce l'analisi iniziale.
Il tutto è gestito in un blocco try-except per catturare e loggare eventuali errori.
L'utilizzo di logger attraverso tutto il codice permette di tenere traccia dettagliata del processo e dei suoi risultati, facilitando l'identificazione di eventuali problemi nei dati.
from functools import partial
logger = logging.getLogger(__name__)
@dataclass
class DatasetMetrics:
rows: int
columns: int
missing_data: Dict[str, float]
duplicates: int
duplicate_percentage: float
dtypes: Dict[str, str]
unique_counts: Dict[str, int]
class DataValidator:
def __init__(self, max_workers: int = 4):
self.max_workers = max_workers
@staticmethod
def _calculate_column_metrics(df: pd.DataFrame, column: str) -> Dict[str, Any]:
return {
'tipo': str(df[column].dtype),
'non_nulli': df[column].count(),
'nulli': df[column].isnull().sum(),
'perc_nulli': (df[column].isnull().sum() / len(df) * 100).round(2),
'unique_values': df[column].nunique()
}
def validate_dataset(self, df: pd.DataFrame, dataset_name: str) -> DatasetMetrics:
logger.info(f"\nValidazione dataset: {dataset_name}")
logger.info("-" * 50)
# Calcolo metriche base
rows, cols = df.shape
logger.info(f"Dimensioni: {rows:,} righe, {cols} colonne")
# Calcolo metriche per colonna in parallelo
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
column_metrics = dict(zip(
df.columns,
executor.map(partial(self._calculate_column_metrics, df), df.columns)
))
# Calcolo duplicati
duplicates = df.duplicated().sum()
duplicate_percentage = (duplicates/len(df)*100).round(2)
logger.info(f"\nDuplicati trovati: {duplicates:,} ({duplicate_percentage}%)")
return DatasetMetrics(
rows=rows,
columns=cols,
missing_data={col: metrics['perc_nulli']
for col, metrics in column_metrics.items()},
duplicates=duplicates,
duplicate_percentage=duplicate_percentage,
dtypes={col: metrics['tipo']
for col, metrics in column_metrics.items()},
unique_counts={col: metrics['unique_values']
for col, metrics in column_metrics.items()}
)
class DataConsistencyChecker:
@staticmethod
def check_consistency(apps_df: pd.DataFrame,
reviews_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
logger.info("\nVerifica consistenza tra dataset")
logger.info("-" * 50)
# Verifica app presenti in entrambi i dataset
apps_in_store = set(apps_df['App'].unique())
apps_in_reviews = set(reviews_df['App'].unique())
common_apps = apps_in_store.intersection(apps_in_reviews)
missing_apps = apps_in_reviews - apps_in_store
logger.info(f"App nel Play Store: {len(apps_in_store):,}")
logger.info(f"App con recensioni: {len(apps_in_reviews):,}")
logger.info(f"App in comune: {len(common_apps):,}")
if missing_apps:
logger.warning(
f"\nAttenzione: {len(missing_apps):,} app hanno recensioni "
"ma non sono nel dataset principale"
)
# Verifica integrità
logger.info("\nVerifica integrità dei dati:")
logger.info(f"- App univoche nel Play Store: {apps_df['App'].nunique():,}")
logger.info(f"- Record totali recensioni: {len(reviews_df):,}")
return apps_df, reviews_df
class InitialAnalyzer:
@staticmethod
def analyze_dataset(df: pd.DataFrame, dataset_name: str) -> None:
logger.info(f"\nAnalisi iniziale: {dataset_name}")
logger.info("-" * 50)
# Analisi colonne numeriche
numeric_cols = df.select_dtypes(include=[np.number]).columns
if len(numeric_cols) > 0:
logger.info("\nStatistiche colonne numeriche:")
logger.info(df[numeric_cols].describe().round(2))
# Analisi colonne categoriche
categorical_cols = df.select_dtypes(include=['object']).columns
if len(categorical_cols) > 0:
logger.info("\nStatistiche colonne categoriche:")
for col in categorical_cols:
unique_vals = df[col].nunique()
logger.info(f"\n{col}:")
logger.info(f"- Valori unici: {unique_vals:,}")
if unique_vals < 10:
dist = df[col].value_counts(normalize=True).head()
logger.info(f"Distribuzione:\n{dist.round(3)}")
def validate_and_analyze_data(apps_df: pd.DataFrame,
reviews_df: pd.DataFrame,
validator: Optional[DataValidator] = None,
consistency_checker: Optional[DataConsistencyChecker] = None,
initial_analyzer: Optional[InitialAnalyzer] = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
logger.info("=== INIZIO VALIDAZIONE E ANALISI DATI ===")
# Inizializzazione componenti se non forniti
validator = validator or DataValidator()
consistency_checker = consistency_checker or DataConsistencyChecker()
initial_analyzer = initial_analyzer or InitialAnalyzer()
try:
# Validazione dataset
apps_metrics = validator.validate_dataset(apps_df, "Google Play Store Apps")
reviews_metrics = validator.validate_dataset(reviews_df, "App Reviews")
# Verifica della consistenza
apps_df, reviews_df = consistency_checker.check_consistency(apps_df, reviews_df)
# Analisi iniziale
initial_analyzer.analyze_dataset(apps_df, "Google Play Store Apps")
initial_analyzer.analyze_dataset(reviews_df, "App Reviews")
return apps_df, reviews_df
except Exception as e:
logger.error(f"Errore durante la validazione e analisi: {str(e)}")
raise
# Esecuzione della validazione e analisi
apps_df, reviews_df = validate_and_analyze_data(apps_df, reviews_df)
WARNING:__main__: Attenzione: 54 app hanno recensioni ma non sono nel dataset principale
3. Pulizia e preparazione dei dati¶
La terza cella di codice si concentra sulla pulizia e preparazione dei dati, una fase estremamente importante per garantire che la successiva analisi sia basata su informazioni accurate e ben strutturate.
clean_size()converte le dimensioni delle app in megabyte. Gestisce casi come 'Varies with device' e converte automaticamente i KB in MB dividendo per 1024 quando necessario. Utilizza espressioni regolari conre.sub(r'[^0-9.]', '')per estrarre solo i numeri dalla stringa;clean_price()standardizza i valori di prezzo in formato numerico, convertendo stringhe come '$4.99' in valori decimali (4.99) e gestendo casi speciali come 'Free' che vengono trasformati in 0.0;clean_installs()converte il numero di installazioni in interi, rimuovendo caratteri come virgole e il segno '+' (es. '1,000,000+' diventa 1000000);clean_android_version()estrae e normalizza la versione Android, utilizzando un'espressione regolare per trovare il formato numerico principale (es. da "Android 4.0.3 or up" estrae 4.0).
Da questa classe base derivano due classi specializzate. La prima è AppsDataCleaner, dedicata alla pulizia del dataset delle applicazioni. Questa classe implementa _parallel_clean_row(), che pulisce una singola riga applicando i metodi di pulizia e aggiungendo nuove colonne pulite e clean_apps_dataset(), che coordina l'intero processo utilizzando ThreadPoolExecutor per parallelizzare la pulizia e migliorare le performance.
Durante il processo di pulizia delle app vengono eseguite diverse operazioni avanzate:
- conversione dei tipi di dati usando
pd.to_numeric()epd.to_datetime(); - rimozione delle righe con valori mancanti attraverso
dropna(); - feature engineering con la creazione della variabile
Days_Since_Update, che indica il numero di giorni trascorsi dall'ultimo aggiornamento di ogni record, allo scopo di fornire informazioni temporali utili per l'analisi dei dati; - categorizzazioni di variabili continue come prezzo e installazioni usando
pd.cut(); - calcolo di metriche di mercato come
Market_ShareeCategory_Share.
La seconda classe derivata, ReviewsDataCleaner, è specifica per il dataset delle recensioni. Sebbene più semplice, si occupa di svolgere diverse attività, ovvero:
- pulire i valori mancanti nelle colonne del sentiment;
- convertire dei tipi di dati per le metriche di polarità e soggettività;
- creare la feature
Review_Lengthper analizzare la lunghezza delle recensioni; - categorizzare la polarità del sentiment in "Negative", "Neutral" e "Positive".
Ci tengo a specificare che queste funzionalità non vengono poi sfruttate appieno nelle analisi successive. Inizialmente avevo pianificato di sviluppare visualizzazioni e insights basati sul sentiment e sulle recensioni degli utenti, ma durante l'analisi ho riscontrato che questi dati non producevano risultati sufficientemente significativi o interpretabili per gli obiettivi del progetto. Ho deciso quindi di focalizzare l'attenzione sull'analisi delle metriche delle app (rating, installazioni, prezzo) che hanno fornito insights più concreti per identificare le opportunità di mercato. Nonostante le recensioni non vengano utilizzate nelle analisi successive, ho mantenuto questa parte del codice di pulizia per completezza metodologica e per eventuali approfondimenti futuri.
Infine, la funzione principale clean_datasets() gestisce l'intero processo di pulizia. Inizializza i cleaner necessari, esegue la pulizia di entrambi i dataset e registra statistiche dettagliate sulle trasformazioni effettuate. Il tutto è incapsulato in un blocco try-except per gestire eventuali errori durante il processo.
Il risultato finale sono due DataFrame puliti e arricchiti, apps_clean e reviews_clean, pronti per le analisi esplorative successive.
logger = logging.getLogger(__name__)
@dataclass
class CleaningReport:
original_rows: int
cleaned_rows: int
removed_rows: int
missing_before: Dict[str, int]
missing_after: Dict[str, int]
cleaning_steps: list[str]
class DataCleaner:
@staticmethod
def clean_size(size: str) -> Optional[float]:
"""Converte la dimensione dell'app in MB"""
if pd.isna(size) or size == 'Varies with device':
return np.nan
try:
size_str = str(size).strip().upper()
multiplier = 1/1024 if 'K' in size_str else 1
return float(re.sub(r'[^0-9.]', '', size_str)) * multiplier
except (ValueError, AttributeError):
return np.nan
@staticmethod
def clean_price(price: str) -> float:
"""Converte il prezzo in valore numerico"""
if pd.isna(price) or price in ['Free', '0', 'Everyone']:
return 0.0
try:
return float(re.sub(r'[^0-9.]', '', str(price)))
except (ValueError, AttributeError):
return 0.0
@staticmethod
def clean_installs(installs: str) -> int:
"""Converte il numero di installazioni in valore numerico"""
if pd.isna(installs):
return 0
try:
return int(str(installs).replace(',', '').replace('+', '').strip())
except ValueError:
return 0
@staticmethod
def clean_android_version(version: str) -> Optional[float]:
"""Estrae e normalizza la versione Android"""
if pd.isna(version):
return np.nan
try:
match = re.search(r'(\d+\.?\d?)', str(version))
return round(float(match.group(1)), 1) if match else np.nan
except (ValueError, AttributeError):
return np.nan
class AppsDataCleaner(DataCleaner):
def __init__(self, max_workers: int = 4):
self.max_workers = max_workers
self.cleaning_steps = []
def _parallel_clean_row(self, row: pd.Series) -> pd.Series:
"""Pulisce una singola riga del dataset in parallelo"""
row = row.copy()
row['Size_MB'] = self.clean_size(row['Size'])
row['Price_Clean'] = self.clean_price(row['Price'])
row['Installs_Clean'] = self.clean_installs(row['Installs'])
row['Android_Ver_Clean'] = self.clean_android_version(row['Android Ver'])
return row
def clean_apps_dataset(self, df: pd.DataFrame) -> Tuple[pd.DataFrame, CleaningReport]:
"""Pulisce il dataset delle applicazioni"""
logger.info("Pulizia dataset applicazioni in corso...")
df_clean = df.copy()
original_rows = len(df_clean)
missing_before = df_clean.isnull().sum().to_dict()
# Pulizia parallela delle righe
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
cleaned_rows = list(executor.map(self._parallel_clean_row, [row for _, row in df_clean.iterrows()]))
df_clean = pd.DataFrame(cleaned_rows, index=df_clean.index)
# Conversione dei tipi di dato
df_clean['Rating'] = pd.to_numeric(df_clean['Rating'], errors='coerce')
df_clean['Reviews'] = pd.to_numeric(df_clean['Reviews'], errors='coerce')
df_clean['Last Updated'] = pd.to_datetime(df_clean['Last Updated'], errors='coerce')
# Rimozione righe con valori mancanti critici
df_clean = df_clean.dropna(subset=['Android_Ver_Clean'])
# Feature engineering
df_clean['Days_Since_Update'] = (pd.Timestamp.now() - df_clean['Last Updated']).dt.days
# Categorizzazione
df_clean['Price_Category'] = pd.cut(
df_clean['Price_Clean'],
bins=[-np.inf, 0, 0.99, 2.99, 4.99, np.inf],
labels=['Free', 'Very Low', 'Low', 'Medium', 'Premium']
)
df_clean['Install_Category'] = pd.cut(
df_clean['Installs_Clean'],
bins=[0, 1000, 100000, 1000000, 10000000, np.inf],
labels=['Very Low', 'Low', 'Medium', 'High', 'Very High']
)
# Calcolo metriche di mercato
total_installs = df_clean['Installs_Clean'].sum()
df_clean['Market_Share'] = df_clean['Installs_Clean'] / total_installs
df_clean['Category_Share'] = df_clean.groupby('Category')['Installs_Clean'].transform(
lambda x: x / x.sum()
)
# Report pulizia
cleaning_report = CleaningReport(
original_rows=original_rows,
cleaned_rows=len(df_clean),
removed_rows=original_rows - len(df_clean),
missing_before=missing_before,
missing_after=df_clean.isnull().sum().to_dict(),
cleaning_steps=self.cleaning_steps
)
return df_clean, cleaning_report
class ReviewsDataCleaner(DataCleaner):
def clean_reviews_dataset(self, df: pd.DataFrame) -> Tuple[pd.DataFrame, CleaningReport]:
"""Pulisce il dataset delle recensioni"""
logger.info("Pulizia dataset recensioni in corso...")
df_clean = df.copy()
original_rows = len(df_clean)
missing_before = df_clean.isnull().sum().to_dict()
# Pulizia valori mancanti
df_clean = df_clean.dropna(subset=['Sentiment', 'Sentiment_Polarity'])
# Conversione tipi di dato
df_clean['Sentiment_Polarity'] = pd.to_numeric(df_clean['Sentiment_Polarity'], errors='coerce')
df_clean['Sentiment_Subjectivity'] = pd.to_numeric(df_clean['Sentiment_Subjectivity'], errors='coerce')
# Feature engineering
df_clean['Review_Length'] = df_clean['Translated_Review'].str.len()
# Categorizzazione sentiment
df_clean['Sentiment_Category'] = pd.cut(
df_clean['Sentiment_Polarity'],
bins=[-1, -0.33, 0.33, 1],
labels=['Negative', 'Neutral', 'Positive']
)
# Report pulizia
cleaning_report = CleaningReport(
original_rows=original_rows,
cleaned_rows=len(df_clean),
removed_rows=original_rows - len(df_clean),
missing_before=missing_before,
missing_after=df_clean.isnull().sum().to_dict(),
cleaning_steps=[]
)
return df_clean, cleaning_report
def clean_datasets(apps_df: pd.DataFrame,
reviews_df: pd.DataFrame,
apps_cleaner: Optional[AppsDataCleaner] = None,
reviews_cleaner: Optional[ReviewsDataCleaner] = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
logger.info("=== INIZIO PROCESSO DI PULIZIA ===")
# Inizializzazione cleaners se non forniti
apps_cleaner = apps_cleaner or AppsDataCleaner()
reviews_cleaner = reviews_cleaner or ReviewsDataCleaner()
try:
# Pulizia dataset app
apps_clean, apps_report = apps_cleaner.clean_apps_dataset(apps_df)
logger.info(f"\nPulizia apps dataset completata:")
logger.info(f"Righe originali: {apps_report.original_rows:,}")
logger.info(f"Righe dopo pulizia: {apps_report.cleaned_rows:,}")
logger.info(f"Righe rimosse: {apps_report.removed_rows:,}")
# Pulizia dataset recensioni
reviews_clean, reviews_report = reviews_cleaner.clean_reviews_dataset(reviews_df)
logger.info(f"\nPulizia reviews dataset completata:")
logger.info(f"Righe originali: {reviews_report.original_rows:,}")
logger.info(f"Righe dopo pulizia: {reviews_report.cleaned_rows:,}")
logger.info(f"Righe rimosse: {reviews_report.removed_rows:,}")
return apps_clean, reviews_clean
except Exception as e:
logger.error(f"Errore durante la pulizia dei dati: {str(e)}")
raise
# Esecuzione della pulizia
apps_clean, reviews_clean = clean_datasets(apps_df, reviews_df)
4. Analisi esplorativa dei dati¶
Questa cella di codice si concentra sull'analisi esplorativa dei dati puliti delle app per ottenere informazioni utili sul mercato ed in essa vengono definite classi e funzioni per eseguire analisi statistiche e generare visualizzazioni.
La prima parte del codice definisce due classi utilizzando il decoratore @dataclass:
CategoryStats: classe utilizzata per memorizzare le informazioni statistiche sulle categorie di app. Include campi come il numero di app in una categoria, il rating medio, la percentuale di app a pagamento, il numero medio di installazioni e la dimensione media delle app;MarketAnalysis: classe utilizzata per contenere i risultati dell'analisi di mercato. Include un dataframe pandas con le statistiche per categoria, un dizionario con le statistiche relative ai prezzi, un dataframe pandas con l'analisi della competitività del mercato e una lista per memorizzare le figure plotly generate dall'analisi.
La classe principale di questa sezione è MarketAnalyzer, che prende come input il dataframe apps_df pulito durante l'inizializzazione. La classe include metodi per calcolare statistiche, creare visualizzazioni ed eseguire diversi tipi di analisi di mercato.
Il metodo __init__ inizializza MarketAnalyzer filtrando le righe in cui Category è '1.9' e imposta il numero di thread che verranno usati per i calcoli in parallelo tramite l'argomento max_workers.
Il metodo _calculate_category_stats è un metodo helper, decorato con @lru_cache(maxsize=None), che calcola e restituisce CategoryStats per una data categoria. Il decoratore @lru_cache memorizza i risultati di questo metodo per efficienza, evitando calcoli ridondanti quando viene chiamato più volte con la stessa categoria.
Il metodo analyze_category_distribution analizza la distribuzione delle app tra le diverse categorie. Utilizza un ThreadPoolExecutor per parallelizzare il calcolo delle statistiche per categoria, crea un dataframe pandas con le statistiche calcolate e genera un grafico a barre usando plotly.express per visualizzare la distribuzione delle app tra le categorie, usando il rating medio come sfumatura di colore.
Il metodo analyze_price_distribution analizza invece la distribuzione dei prezzi delle app a pagamento. Filtra apps_df per includere solo le app a pagamento, definisce gli intervalli di prezzo e le relative etichette per raggruppare i prezzi in categorie, calcola le statistiche dei prezzi (come il numero totale di app, il numero di app a pagamento, la percentuale di app a pagamento e il prezzo medio, mediano e massimo delle app a pagamento), aggrega i dati per intervallo di prezzo e genera un grafico a barre con plotly.graph_objects che ha lo scopo di mostrare la distribuzione dei prezzi usando la percentuale di app per ciascun intervallo di prezzo sull'asse y e gli intervalli di prezzo sull'asse x. Le barre sono colorate in base al rating medio all'interno di ciascun intervallo di prezzo.
Il metodo analyze_market_competition ha l'obiettivo di rappresentare visivamente la competitività del mercato delle app attraverso una mappa che mette in relazione diverse metriche chiave.
Per costruire questa mappa, il codice inizia raggruppando il dataframe apps_df per categoria ("Category") e calcolando per ciascuna di essa quattro metriche fondamentali:
- il numero di app che, in questo caso, indica direttamente il livello di competizione;
- il rating medio, che riflette la qualità percepita dagli utenti;
- la percentuale di app a pagamento, che rappresenta la propensione al pagamento in quella categoria;
- il numero medio di installazioni, che offre una misura dell'ampiezza del mercato.
La propensione al pagamento (paid_perc) viene calcolata con un'espressione lambda 'Price_Clean': lambda x: (x > 0).mean() * 100. Questa formula trasforma i prezzi delle app in valori booleani (True per app a pagamento, False per app gratuite), ne calcola la media (ottenendo la proporzione di app a pagamento) e moltiplica per 100 per esprimere il risultato in percentuale.
Nello specifico:
(x > 0)crea un array di valori booleani doveTruerappresenta le app con prezzo maggiore di zero (app a pagamento) eFalsequelle gratuite;.mean()calcola la media di questi valori booleani, che equivale alla proporzione di app a pagamento (un valore tra 0 e 1);* 100converte questa proporzione in una percentuale.
Questa metrica è importante perché fornisce un'indicazione della disponibilità degli utenti di quella categoria a pagare per le app. Una percentuale più alta suggerisce che:
- gli utenti in quella categoria sono più disposti a pagare per contenuti e funzionalità;
- esiste un precedente di monetizzazione diretta che nuovi entranti potrebbero sfruttare;
- il modello di business "premium" (pagamento diretto) potrebbe essere più facilmente accettato rispetto a modelli freemium o basati sulla pubblicità.
Successivamente, viene calcolato un "indice di dimensione del mercato" (market_size_index) basato sul numero di app in ogni categoria, normalizzato tra 0 e 1. Questo indice rappresenta la dimensione relativa di ciascuna categoria rispetto alle altre, dove una categoria con più app avrà un indice più alto, indicando un mercato più grande e potenzialmente più competitivo.
La visualizzazione vera e propria è realizzata con un grafico a dispersione, creato con plotly.graph_objects. Ogni punto sul grafico rappresenta una categoria di app, posizionata secondo due dimensioni principali:
- l'asse x rappresenta il numero di app nella categoria, ovvero il livello di competizione. Più a destra si trova un punto, maggiore sarà il numero di app in quella categoria e quindi più alta la competizione;
- l'asse y rappresenta il rating medio delle app nella categoria. Più in alto si trova un punto, maggiore sarà la qualità percepita delle app in quella categoria.
La dimensione di ciascun punto è proporzionale all'indice di dimensione del mercato calcolato in precedenza. Quindi, punti più grandi indicano categorie con un mercato potenzialmente più ampio. Il colore di ciascun punto rappresenta il rating medio delle app in quella categoria, usando una scala di colori dal rosso (rating basso) al verde (rating alto), fornendo una ridondanza visiva che rafforza l'informazione dell'asse y e permette di identificare rapidamente le categorie con app percepite come di qualità superiore.
Infine, il metodo calcola un opportunity_score per ciascuna categoria, basato su una combinazione ponderata di tre fattori:
- 40% dal rating medio, premiando le categorie con app di qualità superiore;
- 30% dall'inverso dell'indice di dimensione del mercato, favorendo le categorie meno competitive;
- 30% dalla percentuale di app a pagamento, valorizzando le categorie dove esiste una cultura dell'acquisto.
L'idea alla base di questo punteggio è che le categorie con alto rating, bassa competizione e alta percentuale di app a pagamento rappresentano potenzialmente le migliori opportunità di mercato, andando a bilanciare qualità, facilità di ingresso e potenziale di monetizzazione diretta. La visualizzazione è stata ulteriormente arricchita con un'annotazione che mostra le migliori 5 categorie in base a questo opportunity score.
La funzione perform_exploratory_analysis gestisce l'intero processo di analisi esplorativa dei dati. Inizializza MarketAnalyzer se non viene fornito negli argomenti della funzione, chiama i metodi di analisi di MarketAnalyzer per ottenere i risultati e le relative visualizzazioni, calcola le prime 5 opportunità di mercato in base all'opportunity score e restituisce un oggetto MarketAnalysis contenente i risultati e tutte i grafici generati.
Infine, il codice esegue l'analisi esplorativa chiamando perform_exploratory_analysis - utilizzando i dataframe puliti apps_clean e reviews_clean - e visualizza le figure generate iterando attraverso le figures nell'oggetto market_analysis e usando il metodo show() per visualizzare ciascun grafico plotly nell'output.
Interpretazione dei risultati¶
Distribuzione delle app per categoria e rating medio¶
Il primo grafico offre una panoramica sulla distribuzione delle app nel Google Play Store. Ciò che colpisce immediatamente è la predominanza della categoria "FAMILY", che ospita quasi 2000 applicazioni, rappresentando il segmento di mercato più ampio. Al secondo posto troviamo "GAME" con più di 1000 app, che riflette comunque la significativa popolarità del gaming mobile e la sua capacità di generare ricavi importanti. Più distanziata troviamo "TOOLS" con circa 750 app, una categoria che racchiude utility e strumenti di vario genere.
La visualizzazione rivela un ecosistema di app estremamente disomogeneo, dove poche categorie raccolgono la maggior parte delle applicazioni, mentre molte altre rappresentano nicchie con una presenza numerica decisamente più contenuta. Categorie come "EVENTS", "BEAUTY", "PARENTING" e "WEATHER" hanno una presenza molto limitata, suggerendo potenziali opportunità in mercati meno saturi.
Piuttosto interessante è la relazione tra numero di app e valutazione (rating) media, evidenziata dal sistema di colorazione. Le categorie con un minor numero di applicazioni tendono ad avere rating medi più elevati (visualizzati in verde), come nel caso di "EVENTS", "EDUCATION" e "BOOKS_AND_REFERENCE". Questo fenomeno potrebbe indicare che in mercati meno affollati è più facile emergere con prodotti di qualità che soddisfano più facilmente le aspettative degli utenti. Al contrario, categorie molto competitive come "DATING" e "CASINO" mostrano rating mediamente più bassi (in arancione-rosso), suggerendo una maggiore difficoltà nel distinguersi e soddisfare pienamente le aspettative degli utenti in mercati tendenzialmente saturi.
Distribuzione dei prezzi delle app a pagamento¶
Il secondo grafico ci porta a esplorare le strategie di monetizzazione diretta attraverso la distribuzione dei prezzi delle app a pagamento. Il mercato mostra una chiara preferenza per il pricing nella fascia 1-2.99$, che raccoglie nientemeno che il 36.4% delle app a pagamento. Questo dato suggerisce un punto di equilibrio tra l'accessibilità per gli utenti e il valore percepito da parte degli sviluppatori.
È interessante notare come la distribuzione non segua un andamento lineare decrescente: la fascia 0-1$ (20.2%) è più popolata della fascia 3-4.99$ (19.9%), mentre c'è un netto calo nella fascia 5-9.99$ (11.3%) prima di una leggera risalita nella categoria premium oltre i 10$ (12.1%). Questo pattern riflette le diverse strategie di monetizzazione e posizionamento: molti sviluppatori optano per prezzi molto bassi puntando sul volume, mentre altri scelgono un posizionamento premium con prezzi elevati mirando a utenti disposti a pagare per funzionalità esclusive o molto specifiche.
La colorazione delle barre nel grafico offre un'ulteriore dimensione analitica. Esaminando le sfumature cromatiche, emerge che le applicazioni nelle fasce di prezzo inferiori presentano valutazioni mediamente superiori, evidenziando una corrispondenza efficace tra il valore economico richiesto e la soddisfazione dell′utenza. Questo fenomeno suggerisce che le aspettative dei consumatori a questi livelli di prezzo sono generalmente soddisfatte dall′esperienza d′uso. Per contro, le applicazioni posizionate nella fascia premium (oltre 10$) mostrano valutazioni mediamente inferiori, il che potrebbe indicare che gli utenti sono più critici quando pagano prezzi premium e le loro aspettative sono più difficili da soddisfare.
Mappa competitiva del mercato¶
La mappa competitiva rappresenta uno strumento analitico sofisticato che offre una visione multidimensionale del mercato delle app. In questo grafico a dispersione, ogni categoria viene posizionata in base a due metriche fondamentali: il numero di applicazioni (asse X) che indica il livello di competizione, e il rating medio (asse Y) che riflette la soddisfazione degli utenti.
Il panorama competitivo si presenta stratificato, con "FAMILY" che domina in termini quantitativi con circa 2000 app (estrema destra del grafico), seguito da "GAME" con circa 1000 app e "TOOLS" con circa 750 app, come già risultava evidente dal grafico "Distribuzione delle app per categoria e rating medio" La dimensione dei cerchi, proporzionale all'indice di dimensione del mercato, amplifica visivamente questa gerarchia, evidenziando il peso significativo di queste categorie nell'ecosistema complessivo.
Particolarmente interessante è la distribuzione verticale: categorie come "EVENTS", "EDUCATION" e "ART_AND_DESIGN" si collocano nella parte superiore del grafico con rating medi superiori a 4.3, suggerendo un'elevata qualità percepita. All'estremo opposto, categorie come "DATING" mostrano rating più contenuti, indice di una maggiore difficoltà nel soddisfare le aspettative degli utenti.
L'analisi si arricchisce significativamente grazie al riquadro informativo presente in basso a destra nel grafico, che rivela le migliori 5 opportunità di mercato secondo l'opportunity score:
- MEDICAL emerge come l'opportunità più promettente con uno score di 8.67, combinando un buon rating (4.2), una competizione moderata (439 app) e un'elevata propensione al pagamento (22.6% di app a pagamento);
- PERSONALIZATION si posiziona immediatamente dopo con 8.63, grazie a un rating leggermente superiore (4.3) e un mercato meno affollato (352 app), mantenendo un'alta percentuale di app a pagamento (22.2%);
- BOOKS_AND_REFERENCE presenta uno score di 6.06, con un eccellente rating (4.3) e bassa competizione (200 app), sebbene presenti una propensione al pagamento inferiore (13.5%);
- WEATHER rappresenta un'interessante nicchia con uno score di 5.15, combinando un buon rating (4.2) con una competizione minima (solo 57 app) e una discreta propensione al pagamento (10.5%);
- TOOLS, nonostante l'elevata competizione (744 app), mantiene uno score rispettabile di 4.57 grazie ad rating discreto (4.0) e alla presenza di un buon segmento di utenti disposti a pagare (9.3%).
Questa classifica rivela come le migliori opportunità non siano necessariamente le categorie situate nell'angolo in alto a sinistra del grafico (alto rating, bassa competizione). La propensione al pagamento gioca un ruolo importante, rendendo categorie come MEDICAL e PERSONALIZATION particolarmente attraenti nonostante non siano le meno competitive o quelle con le valutazioni più alte in assoluto.
logger = logging.getLogger(__name__)
@dataclass
class CategoryStats:
num_apps: int
avg_rating: float
paid_perc: float
avg_installs: float
avg_size: float
@dataclass
class MarketAnalysis:
category_stats: pd.DataFrame
price_stats: Dict[str, float]
market_analysis: pd.DataFrame
figures: List[go.Figure] = field(default_factory=list)
class MarketAnalyzer:
def __init__(self, apps_df: pd.DataFrame, max_workers: int = 4):
self.apps_df = apps_df[apps_df['Category'] != '1.9'].copy()
self.max_workers = max_workers
@lru_cache(maxsize=None)
def _calculate_category_stats(self, category: str) -> CategoryStats:
cat_data = self.apps_df[self.apps_df['Category'] == category]
return CategoryStats(
num_apps=len(cat_data),
avg_rating=cat_data['Rating'].mean(),
paid_perc=(cat_data['Price_Clean'] > 0).mean() * 100,
avg_installs=cat_data['Installs_Clean'].mean(),
avg_size=cat_data['Size_MB'].mean()
)
def analyze_category_distribution(self) -> Tuple[pd.DataFrame, go.Figure]:
# Calcolo parallelo delle statistiche per categoria
categories = self.apps_df['Category'].unique()
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
stats = list(executor.map(self._calculate_category_stats, categories))
# Creazione DataFrame
category_stats = pd.DataFrame({
'Category': categories,
'num_apps': [s.num_apps for s in stats],
'avg_rating': [s.avg_rating for s in stats],
'paid_perc': [s.paid_perc for s in stats],
'avg_installs': [s.avg_installs for s in stats],
'avg_size': [s.avg_size for s in stats]
}).round(2)
# Creazione grafico ottimizzato
fig = px.bar(
category_stats,
x='Category',
y='num_apps',
color='avg_rating',
title='Distribuzione delle app per categoria e rating medio',
labels={
'num_apps': 'Numero di app',
'Category': 'Categoria',
'avg_rating': 'Rating medio'
},
color_continuous_scale=[[0, '#B30000'], [0.4, '#FF0000'],
[0.6, '#FFA500'], [0.75, '#2ECC40'],
[1, '#00B300']],
range_color=[3.2, 4.8]
)
fig.update_layout(
xaxis_tickangle=-45,
showlegend=True,
height=600,
title_x=0.5,
font=dict(family="Arial", size=12),
margin=dict(t=100, l=50, r=50, b=100)
)
return category_stats.set_index('Category'), fig
def analyze_price_distribution(self) -> Tuple[Dict[str, float], go.Figure]:
df_paid = self.apps_df[self.apps_df['Price_Clean'] > 0].copy()
price_ranges = [0, 1, 2.99, 4.99, 9.99, float('inf')]
price_labels = ['0-1$', '1-2.99$', '3-4.99$', '5-9.99$', '10$+']
df_paid['price_range'] = pd.cut(df_paid['Price_Clean'],
bins=price_ranges,
labels=price_labels)
# Calcolo statistiche prezzi
price_stats = {
'total_apps': len(self.apps_df),
'paid_apps': len(df_paid),
'paid_percentage': (len(df_paid) / len(self.apps_df)) * 100,
'avg_price': df_paid['Price_Clean'].mean(),
'median_price': df_paid['Price_Clean'].median(),
'max_price': df_paid['Price_Clean'].max()
}
# Aggregazione dati per il grafico
price_distribution = df_paid.groupby('price_range').agg({
'App': 'count',
'Rating': 'mean'
}).reset_index()
price_distribution['percentage'] = (
price_distribution['App'] / len(df_paid)
) * 100
# Creazione grafico
fig = go.Figure(data=[
go.Bar(
x=price_distribution['price_range'],
y=price_distribution['percentage'],
marker=dict(
color=price_distribution['Rating'],
colorscale=[[0, '#B30000'], [0.4, '#FF0000'],
[0.6, '#FFA500'], [0.75, '#2ECC40'],
[1, '#00B300']],
colorbar=dict(
title="Rating medio",
titleside="right",
xpad=30,
len=0.9,
thickness=20
),
cmin=3.2,
cmax=4.8
),
text=price_distribution['percentage'].round(1).astype(str) + '%',
textposition='outside'
)
])
fig.update_layout(
title='Distribuzione dei prezzi delle app a pagamento',
title_x=0.5,
xaxis_title='Fascia di prezzo',
yaxis_title='Percentuale di app (%)',
height=500,
yaxis_range=[0, max(price_distribution['percentage']) * 1.1],
bargap=0.2,
font=dict(family="Arial", size=12)
)
return price_stats, fig
def analyze_market_competition(self) -> Tuple[pd.DataFrame, go.Figure]:
"""Analizza la competitività del mercato"""
market_analysis = self.apps_df.groupby('Category').agg({
'App': 'count',
'Rating': 'mean',
'Price_Clean': lambda x: (x > 0).mean() * 100,
'Installs_Clean': 'mean'
}).round(2)
market_analysis.columns = ['num_apps', 'avg_rating', 'paid_perc', 'avg_installs']
# Calcolo indice dimensione mercato
market_analysis['market_size_index'] = (
(market_analysis['num_apps'] - market_analysis['num_apps'].min()) /
(market_analysis['num_apps'].max() - market_analysis['num_apps'].min())
)
# Creazione grafico
fig = go.Figure(data=[
go.Scatter(
x=market_analysis['num_apps'],
y=market_analysis['avg_rating'],
mode='markers+text',
text=market_analysis.index,
textposition='top center',
marker=dict(
size=market_analysis['market_size_index'] * 50,
color=market_analysis['avg_rating'],
colorscale=[[0, '#B30000'], [0.4, '#FF0000'],
[0.6, '#FFA500'], [0.75, '#2ECC40'],
[1, '#00B300']],
colorbar=dict(
title="Rating medio",
titleside="right",
xpad=30,
len=0.9,
thickness=20
),
cmin=3.2,
cmax=4.8
)
)
])
fig.update_layout(
title='Mappa competitiva del mercato',
title_x=0.5,
xaxis_title='Numero di app (competizione)',
yaxis_title='Rating medio',
height=600,
showlegend=False,
font=dict(family="Arial", size=12)
)
# Calcolo opportunity score
market_analysis['opportunity_score'] = (
market_analysis['avg_rating'] * 0.4 +
(1 - market_analysis['market_size_index']) * 0.3 +
market_analysis['paid_perc'] * 0.3
)
# Aggiungi annotazione con migliori opportunità
top_opportunities = market_analysis.nlargest(5, 'opportunity_score')
top_text = "<b>Top 5 opportunità di mercato:</b><br>"
for cat in top_opportunities.index:
score = market_analysis.loc[cat, 'opportunity_score']
rating = market_analysis.loc[cat, 'avg_rating']
apps = market_analysis.loc[cat, 'num_apps']
paid = market_analysis.loc[cat, 'paid_perc']
top_text += f"<b>{cat}</b>: score {score:.2f} (rating {rating:.1f}, app {apps}, propensione {paid:.1f}%)<br>"
fig.add_annotation(
x=0.99,
y=0.01,
xref="paper",
yref="paper",
xanchor="right",
yanchor="bottom",
text=top_text,
showarrow=False,
font=dict(size=11),
align="left",
bgcolor="rgba(255, 255, 255, 0.9)",
bordercolor="black",
borderwidth=1,
borderpad=6
)
return market_analysis, fig
def perform_exploratory_analysis(apps_df: pd.DataFrame,
reviews_df: Optional[pd.DataFrame] = None,
analyzer: Optional[MarketAnalyzer] = None) -> MarketAnalysis:
logger.info("=== INIZIO ANALISI ESPLORATIVA ===")
analyzer = analyzer or MarketAnalyzer(apps_df)
figures = []
try:
# 1. Analisi distribuzione per categoria
logger.info("\n1. Analisi distribuzione per categoria")
category_stats, category_fig = analyzer.analyze_category_distribution()
figures.append(category_fig)
# 2. Analisi prezzi
logger.info("\n2. Analisi distribuzione prezzi")
price_stats, price_fig = analyzer.analyze_price_distribution()
figures.append(price_fig)
# 3. Analisi competitiva
logger.info("\n3. Analisi competitiva del mercato")
market_analysis, market_fig = analyzer.analyze_market_competition()
figures.append(market_fig)
# Log risultati principali
logger.info("\nTop 5 opportunità di mercato:")
top_opportunities = market_analysis.nlargest(5, 'opportunity_score')
for cat in top_opportunities.index:
logger.info(f"\n{cat}:")
logger.info(f"- Score: {market_analysis.loc[cat, 'opportunity_score']:.2f}")
logger.info(f"- Rating medio: {market_analysis.loc[cat, 'avg_rating']:.2f}")
logger.info(f"- Competizione: {market_analysis.loc[cat, 'num_apps']:,} app")
logger.info(f"- % app a pagamento: {market_analysis.loc[cat, 'paid_perc']:.1f}%")
return MarketAnalysis(
category_stats=category_stats,
price_stats=price_stats,
market_analysis=market_analysis,
figures=figures
)
except Exception as e:
logger.error(f"Errore durante l'analisi esplorativa: {str(e)}")
raise
# Esecuzione dell'analisi esplorativa
market_analysis = perform_exploratory_analysis(apps_clean, reviews_clean)
# Visualizzazione dei grafici
for fig in market_analysis.figures:
fig.show()
5. Analisi delle performance e delle metriche chiave¶
La quinta cella rappresenta il passaggio da un'esplorazione descrittiva iniziale a un'indagine più approfondita delle relazioni tra variabili e dei trend temporali. Se nei blocchi precedenti sono state identificate le opportunità di mercato in base alle categorie, ora esploriamo come diverse metriche si influenzano reciprocamente e come il mercato si è evoluto nel tempo.
Vengono definite due classi NamedTuple che fungono da contenitori tipizzati per i risultati: CorrelationResults raccoglie le matrici di correlazione di Pearson e Spearman insieme alle loro visualizzazioni, mentre TimeMetrics contiene i dati temporali aggregati e il grafico corrispondente.
La classe principale PlayStoreAnalyzer è implementata come @dataclass e nel suo metodo __post_init__ viene eseguita un'operazione di filtraggio necessaria:
self.apps_df = self.apps_df[self.apps_df['Category'] != '1.9'].copy()
Questa riga rimuove dal dataset un'osservazione chiaramente anomala in cui la colonna "Category" contiene il valore '1.9', che non corrisponde a nessuna categoria legittima del Google Play Store. Andando ad analizzare più attentamente il dataset si può notare che si tratta di un record con un disallineamento dei dati, dove i valori sono stati spostati di posizione tra le colonne. In questa riga, infatti, troviamo valori come un rating impossibile di "19", la stringa "Free" nella colonna delle installazioni e altri elementi chiaramente fuori posto. Ho quindi deciso di rimuovere completamente questi dati che comprometterebbero l'affidabilità delle analisi successive.
Dopo questa pulizia iniziale, il metodo prepare_data() trasforma i dati grezzi in metriche analitiche significative attraverso diverse operazioni essenziali:
elabora le informazioni temporali convertendo la colonna "Last Updated" in formato datetime, calcolando il numero di giorni trascorsi dall'ultimo aggiornamento rispetto alla data più recente nel dataset ed estraendo l'anno di aggiornamento in una nuova colonna;
applica trasformazioni logaritmiche (
np.log1p())alle colonne delle installazioni e delle dimensioni. Questa tecnica è particolarmente utile per gestire distribuzioni asimmetriche o con ampi intervalli di valori, permettendo pertanti di visualizzare e analizzare meglio relazioni che altrimenti sarebbero difficili da interpretare.
Il codice calcola anche metriche di mercato come la quota di mercato globale (dividendo le installazioni di ogni app per il totale delle installazioni) e la quota all'interno della categoria, utilizzando la funzione transform di pandas che permette di mantenere la dimensionalità originale del dataframe.
Inoltre, il metodo prevede l'integrazione dei dati di sentiment nel dataset principale attraverso _merge_sentiment_data(). Questo processo avviene in tre passaggi: prima unisce i dati delle recensioni con le categorie delle app tramite un inner join, poi aggrega i dati per calcolare la polarità media (positività/negatività) e la soggettività media delle recensioni per ciascuna app e infine unisce questi dati aggregati al dataframe principale con un left join.
Ci tengo comunque anche in questo caso a specificare che metriche di sentiment (Sentiment_Polarity e Sentiment_Subjectivity) non vengono effettivamente utilizzate nelle visualizzazioni e nelle analisi di correlazione per le motivazioni espresse precedentemente.
Il metodo analyze_correlations() calcola due tipi di coefficienti di correlazione che forniscono prospettive complementari:
la correlazione di Pearson (r = Σ[(x_i - x̄)(y_i - ȳ)] / √[Σ(x_i - x̄)² · Σ(y_i - ȳ)²] - in cui x_i e y_i sono le osservazioni individuali e x̄ e ȳ sono le medie delle variabili) misura relazioni lineari tra variabili, quantificando quanto due variabili tendono ad aumentare o diminuire insieme in modo proporzionale. È particolarmente efficace per relazioni lineari, ma può essere fuorviante in presenza di relazioni non lineari o outlier significativi;
la correlazione di Spearman (ρ = 1 - (6 · Σd_i²) / [n(n² - 1)] - in cui d_i è la differenza tra i ranghi delle osservazioni corrispondenti e n è il numero di osservazioni) cattura relazioni monotoniche - ovvero quando una variabile aumenta, l'altra tende a cambiare sempre nella stessa direzione - anche quando non sono strettamente lineari ed è basata sui ranghi delle variabili anziché sui loro valori assoluti. Essendo calcolata sui ranghi, risulta più robusta rispetto ad outlier e distribuzioni non normali, fornendo pertanto una visione più completa quando si analizzano dati come quelli in questione che spesso presentano distribuzioni asimmetriche.
Il calcolo delle correlazioni viene eseguito utilizzando le funzionalità integrate di pandas:
# Calcolo correlazioni
data = self.apps_df[metrics.keys()]
pearson_corr = data.corr(method='pearson').round(3)
spearman_corr = data.corr(method='spearman').round(3)
Il metodo round(3) arrotonda i coefficienti di correlazione a tre decimali per migliorare la leggibilità.
L'utilizzo di entrambe le metriche offre una visione potenzialmente più completa delle relazioni tra variabili, permettendo di identificare sia pattern lineari che non lineari nei dati.
Per visualizzare queste correlazioni, il codice crea due rappresentazioni principali. La prima è una heatmap che confronta affiancate le matrici di correlazione di Pearson e Spearman, utilizzando una scala di colori dal rosso (correlazioni negative) al blu (correlazioni positive) per evidenziare visivamente la forza e la direzione delle relazioni. La seconda è una matrice di scatter plot che mostra le relazioni tra coppie specifiche di variabili. Nel caso dello scatter plot che analizza la relazione tra rating e giorni dall'ultimo aggiornamento il codice aggiunge un leggero rumore casuale ai valori di rating ("jitter") per evitare sovrapposizioni di punti, calcola poi una media mobile per evidenziare la tendenza generale e limita l'asse y al 95° percentile per evitare che valori estremi comprimano la visualizzazione. In questa seconda visualizzazione, il colore dei punti è basato sul rating delle app, con una scala che va dal rosso (rating bassi) al verde (rating alti), permettendo di identificare facilmente pattern nelle relazioni tra variabili.
Il metodo analyze_temporal_trends() offre una prospettiva evolutiva del mercato delle app, aggregando i dati per anno di aggiornamento per comprendere come le metriche chiave sono cambiate nel tempo.
L'analisi inizia calcolando il prezzo medio delle app a pagamento per ogni anno, escludendo le app gratuite per non distorcere i risultati. Vengono poi aggregati i dati per anno, calcolando il rating medio, la dimensione media, le installazioni medie e il conteggio delle app per ogni periodo.
La visualizzazione dell'evoluzione temporale adotta un approccio a due subplot che organizzano le metriche in gruppi concettualmente coerenti:
il subplot superiore presenta le metriche di prodotto, caratteristiche intrinseche delle app: rating medio (che riflette la qualità percepita dagli utenti), prezzo medio (che indica le strategie di monetizzazione) e dimensione media (che, in teoria, può essere correlata alla complessità e ricchezza delle funzionalità). Questi tre parametri vengono visualizzati con linee di colori diversi per facilitarne la distinzione: verde per il rating, rosso per il prezzo e blu per la dimensione;
il subplot inferiore visualizza invece le metriche di mercato, indicatori della performance e della diffusione delle app: installazioni medie (rappresentate da una linea arancione che mostra la popolarità media delle app) e numero totale di app (visualizzato come barre grigie semitrasparenti, che forniscono contesto sull'evoluzione della dimensione del mercato).
Per il subplot inferiore viene implementata una scala logaritmica sull'asse Y, che trasforma incrementi esponenziali in incrementi lineari: ad esempio, i passaggi da 1000 a 10000, da 10000 a 100000 e da 100000 a 1000000 appaiono come distanze uguali nel grafico. Questo approccio permette di visualizzare contemporaneamente app con popolarità molto diverse e apprezzare cambiamenti proporzionali anziché assoluti, rivelando pattern di crescita relativi che altrimenti rimarrebbero nascosti in una scala lineare tradizionale.
Infine il metodo _format_trend_value()personalizza il formato in base al tipo di metrica: trasforma grandi numeri di installazioni in formati più leggibili (K per migliaia, M per milioni), formatta i prezzi con il simbolo del dollaro, aggiunge "MB" alle dimensioni e gestisce in modo appropriato i valori mancanti con la notazione "N/D".
La funzione analyze_play_store() coordina il processo di analisi. La sua implementazione segue diverse fasi:
la funzione crea un'istanza di
PlayStoreAnalyzerpassando i dataframe delle app e delle recensioni. Questa istanza contiene tutta la logica specializzata per le diverse analisi. Predispone inoltre due strutture dati vuote: una lista figures per raccogliere le visualizzazioni generate e un dizionario results per memorizzare i risultati numerici.esegue l'analisi delle correlazioni chiamando il metodo
analyze_correlations()all'interno di un blocco try-except per la gestione degli errori. I risultati di questa analisi vengono memorizzati nel dizionarioresultssotto la chiave 'correlations' e le figure generate vengono aggiunte alla listafigures. Una caratteristica importante è l'identificazione e la registrazione delle correlazioni statisticamente rilevanti: la funzione itera attraverso le matrici di correlazione di Pearson e Spearman, estraendo e loggando solo le relazioni con coefficiente di correlazione superiore a 0.3 in valore assoluto.procede poi con l'analisi dei trend temporali chiamando
analyze_temporal_trends(). Anche in questo caso, i risultati e le visualizzazioni vengono memorizzati. Un elemento che merita un minimo di approfondimento è il calcolo dettagliato dei cambiamenti tra il primo e l'ultimo anno disponibile nel dataset: per il rating medio viene calcolata la variazione percentuale, mentre per altre metriche come dimensione media, installazioni medie e numero di app, vengono registrati i valori assoluti all'inizio e alla fine del periodo analizzato.tutti i risultati e le figure vengono raccolti nel dizionario
results, che viene restituito come output della funzione. Il blocco try-except che avvolge l'intera implementazione fornisce robustezza all'analisi:. Quando si verifica un'eccezione, il codice cattura l'errore entrando nel blocco except, lo registra nel sistema di logging attraversologger.error()e lo ri-solleva (ovvero l'eccezione può poi continuare il suo percorso verso l'alto nella pila di chiamate) con l'istruzioneraisesenza parametri.
Questo approccio:
garantisce che nessun errore passi inosservato grazie alla registrazione nel log;
mantiene la traccia completa dell'errore originale (tipo, messaggio e stack trace) e permette al codice chiamante di implementare eventuali strategie di recupero.
A differenza di una gestione che "inghiottirebbe" l'errore, questa tecnica assicura che problemi nei dati o nel processo di analisi vengano correttamente identificati e possano essere affrontati in modo appropriato.
Interpretazione dei risultati¶
Confronto correlazioni (Pearson vs Spearman)¶
Analizzando le matrici, emergono diverse correlazioni interessanti:
Giorni dall'ultimo aggiornamento vs Log dimensione
Pearson: -0.35 / Spearman: -0.33
Questa correlazione negativa indica che app aggiornate più recentemente tendono ad avere dimensioni maggiori. Potrebbe riflettere una tendenza degli sviluppatori a rilasciare aggiornamenti che arricchiscono l'app con nuove funzionalità, aumentandone di conseguenza la dimensione.
Log dimensione vs Log installazioni
Pearson: 0.34 / Spearman: 0.35
Questa correlazione positiva suggerisce che app più grandi tendono ad avere più installazioni. Ciò potrebbe indicare che gli utenti sono disposti a scaricare app più pesanti quando offrono più funzionalità o contenuti, oppure che app di maggior successo tendono ad espandersi nel tempo.
Giorni dall'ultimo aggiornamento vs Log installazioni
Pearson: -0.19 / Spearman: -0.33
È interessante notare come questa correlazione sia più forte secondo Spearman che Pearson, suggerendo una relazione monotonica, ma non perfettamente lineare. App aggiornate più frequentemente tendono ad avere più installazioni, presumibilmente perché gli aggiornamenti regolari mantengono l'app rilevante e attraente per gli utenti.
Prezzo vs altre metriche
Entrambe le matrici mostrano correlazioni deboli tra il prezzo e le altre metriche, con valori che raramente superano ±0.20. Questo suggerisce che il prezzo ha una relazione limitata con gli altri parametri, il che potrebbe indicare che le strategie di prezzo sono determinate da fattori diversi dalla popolarità o dalle caratteristiche tecniche delle app.
Rating vs altre metriche
Anche il rating mostra correlazioni generalmente deboli con le altre metriche, con l'eccezione di una lieve correlazione negativa con i giorni dall'ultimo aggiornamento (-0.13 in Pearson, -0.19 in Spearman), suggerendo che app aggiornate più recentemente tendono ad avere valutazioni leggermente migliori.
Quindi riassumendo, possiamo dedurre che:
la frequenza di aggiornamento sembra essere un fattore importante correlato al successo di un'app, suggerendo l'importanza di una manutenzione regolare;
la dimensione dell'app ha una correlazione positiva con le installazioni, indicando che gli utenti potrebbero preferire app più ricche di funzionalità;
il prezzo e il rating sembrano essere influenzati da fattori più complessi non catturati direttamente dalle altre metriche analizzate.
È tuttavia importante specificare che le correlazioni non sono estremamente forti, ma ci forniscono comunque indicazioni utili sulle relazioni tra le caratteristiche delle app.
Scatter plots delle relazioni principali¶
Questi scatter plots arricchiscono notevolmente la comprensione delle correlazioni analizzate precedentemente:
confermano visivamente la natura delle relazioni, mostrandone non solo la forza, ma anche la forma;
evidenziano pattern non lineari, particolarmente visibili nel grafico Rating vs Giorni da ultimo aggiornamento;
permettono di identificare cluster e distribuzioni che le semplici correlazioni non catturano.
Le implicazioni strategiche che emergono da questa analisi visuale rafforzano quanto già osservato, ovvero:
gli aggiornamenti regolari sembrano essere fondamentali per mantenere rating elevati e, potenzialmente, per aumentare le installazioni
la relazione positiva tra dimensione delle app e loro diffusione suggerisce che gli utenti apprezzano app più ricche di funzionalità;
vi è un'evidente interconnessione tra rating elevato e maggior numero di installazioni, il che può indicare come la qualità percepita possa influenzare la popolarità di un'app.
Evoluzione temporale delle metriche chiave¶
Il grafico offre una prospettiva dinamica sul mercato delle app dal 2010 al 2018, suddivisa in due pannelli: il superiore mostra le metriche di prodotto (rating, prezzo, dimensione) e l'inferiore le metriche di mercato (installazioni e numero di app).
Nel pannello superiore osserviamo alcuni trend trend nelle caratteristiche delle app:
Dimensione media (linea blu): mostra la crescita più importante, passando da valori prossimi allo zero nel 2010 a circa 25 MB nel 2018. Questo incremento costante e sostanziale riflette l'evoluzione delle capacità hardware dei dispositivi mobili e la crescente complessità delle app moderne, che incorporano funzionalità sempre più avanzate, elementi grafici di maggiore qualità e contenuti multimediali.
Prezzo medio (linea rossa): presenta un andamento più irregolare con un'impennata notevole tra il 2016 e il 2017 (da circa 5$ a 22$), seguita da una leggera discesa nel 2018. Questo picco potrebbe indicare un cambiamento nelle strategie di monetizzazione o l'ingresso nel mercato di app premium in categorie specifiche. La successiva diminuzione suggerisce un possibile aggiustamento del mercato verso prezzi più competitivi.
Rating medio (linea verde): rimane notevolmente stabile intorno al valore 4 durante l'intero periodo, con variazioni minime. Questa stabilità è interessante considerando i cambiamenti significativi nelle altre metriche e suggerisce che, nonostante l'evoluzione del mercato, le aspettative relative alla qualità da parte degli utenti utenti e la capacità degli sviluppatori di soddisfarle sono rimaste relativamente costanti.
Nel pannello inferiore, visualizzato in scala logaritmica:
Installazioni medie (linea arancione): mostrano un incremento graduale nel periodo analizzato, con un'accelerazione più marcata negli ultimi anni, raggiungendo oltre un milione di installazioni medie per app nel 2018. Questo trend suggerisce una crescente penetrazione degli smartphone e un aumento dell'engagement degli utenti con le app.
Numero di app (barre grigie): evidenzia una crescita esponenziale, arrivando a contare diverse migliaia di app nel 2018. Questa crescita testimonia l'esplosione dell'ecosistema delle app e l'intensificarsi della competizione. È importante notare che la scala logaritmica del grafico attenua visivamente questa crescita che in realtà è molto più accentuata di quanto appaia.
Analizzando congiuntamente i due pannelli, emergono relazioni interessanti:
dimensione e complessità crescenti: l'aumento costante della dimensione media delle app è avvenuto in parallelo con la crescita delle installazioni medie e ciò suggerisce che gli utenti non sono stati scoraggiati da app più pesanti, probabilmente perché offrono esperienze più ricche e appaganti e funzionalità maggiori;
stabilità della qualità percepita: nonostante l'aumento della complessità e della dimensione delle app, il rating medio è rimasto stabile.
dinamiche di prezzo: ll sensibile aumento dei prezzi tra 2016 e 2017, seguito da un leggero calo, potrebbe riflettere tentativi di monetizzazione più aggressivi in un mercato maturo, seguiti da aggiustamenti competitivi.
saturazione del mercato: La crescita esponenziale del numero di app, combinata con l'aumento più moderato delle installazioni medie, suggerisce una crescente competizione per l'attenzione degli utenti.
Le analisi svolte in questo frammento di codice e le relative visualizzazioni, in conclusione, ci offrono alcune indicazioni preziose per il lancio di una ipotetica nuova app:
gli utenti sembrano accettare app di dimensioni maggiori, purché queste offrano valore proporzionato;
il mercato è diventato estremamente competitivo, con migliaia di app che si contendono l'attenzione degli utenti;
la stabilità delle valutazioni suggerisce che le aspettative di qualità sono ben consolidate;
le strategie di prezzo richiedono particolare attenzione, considerando i cambiamenti significativi osservati negli ultimi anni.
logger = logging.getLogger(__name__)
class CorrelationResults(NamedTuple):
pearson: pd.DataFrame
spearman: pd.DataFrame
figures: List[go.Figure]
class TimeMetrics(NamedTuple):
data: pd.DataFrame
figure: go.Figure
@dataclass
class PlayStoreAnalyzer:
apps_df: pd.DataFrame
reviews_df: Optional[pd.DataFrame] = None
max_workers: int = 4
def __post_init__(self):
self.apps_df = self.apps_df[self.apps_df['Category'] != '1.9'].copy()
self.prepare_data()
def prepare_data(self) -> None:
# Metriche temporali
self.apps_df['Last Updated'] = pd.to_datetime(self.apps_df['Last Updated'])
# Usa la data più recente nel dataset come riferimento
max_date = self.apps_df['Last Updated'].max()
self.apps_df['Days_Since_Update'] = (
max_date - self.apps_df['Last Updated']
).dt.days
self.apps_df['Update_Year'] = self.apps_df['Last Updated'].dt.year
# Trasformazioni logaritmiche
self.apps_df['Log_Installs'] = np.log1p(self.apps_df['Installs_Clean'])
self.apps_df['Log_Size'] = np.log1p(self.apps_df['Size_MB'])
# Metriche di mercato
total_installs = self.apps_df['Installs_Clean'].sum()
self.apps_df['market_share'] = self.apps_df['Installs_Clean'] / total_installs
self.apps_df['category_share'] = self.apps_df.groupby('Category')['Installs_Clean'].transform(
lambda x: x / x.sum()
)
# Merge con sentiment se disponibile
if self.reviews_df is not None:
self._merge_sentiment_data()
def _merge_sentiment_data(self) -> None:
sentiment_data = self.reviews_df.merge(
self.apps_df[['App', 'Category']],
on='App',
how='inner'
)
app_sentiment = sentiment_data.groupby(['App', 'Category']).agg({
'Sentiment_Polarity': 'mean',
'Sentiment_Subjectivity': 'mean'
}).reset_index()
self.apps_df = self.apps_df.merge(
app_sentiment,
on=['App', 'Category'],
how='left'
)
@staticmethod
def _create_correlation_heatmap(corr_matrix: pd.DataFrame,
title: str) -> go.Figure:
return go.Figure(
data=go.Heatmap(
z=corr_matrix.values,
x=corr_matrix.columns,
y=corr_matrix.index,
colorscale='RdBu',
zmin=-1,
zmax=1,
text=corr_matrix.values.round(2),
texttemplate='%{text}',
textfont={"size": 10}
),
layout=dict(
title=title,
height=600,
font=dict(family="Arial", size=12)
)
)
def _create_scatter_matrix(self, metrics: Dict[str, str]) -> go.Figure:
scatter_pairs = [
('Rating', 'Log_Installs'),
('Rating', 'Log_Size'),
('Log_Size', 'Log_Installs'),
('Rating', 'Days_Since_Update')
]
fig = make_subplots(
rows=2, cols=2,
subplot_titles=[
f'{metrics[x]} vs {metrics[y]}'
for x, y in scatter_pairs
]
)
# Creazione di una scala di colori per i giorni
colorscale = [
[0, 'green'], # 0-30 giorni
[0.1, 'lightgreen'], # 30-90 giorni
[0.2, 'yellow'], # 90-180 giorni
[0.4, 'orange'], # 6 mesi-1 anno
[0.6, 'red'], # 1-2 anni
[1.0, 'darkred'] # >2 anni
]
for idx, (x, y) in enumerate(scatter_pairs):
row = idx // 2 + 1
col = idx % 2 + 1
hover_text = [
f"App: {app}<br>" +
f"Categoria: {cat}<br>" +
f"{metrics[x]}: {val_x:.2f}<br>" +
f"{metrics[y]}: {val_y:.2f}<br>" +
f"Prezzo: ${price:.2f}<br>" +
f"Installazioni: {inst:,.0f}<br>" +
f"Dimensione: {size:.1f}MB<br>" +
f"Giorni dall'ultimo aggiornamento: {days:.0f}"
for app, cat, val_x, val_y, price, inst, size, days in zip(
self.apps_df['App'],
self.apps_df['Category'],
self.apps_df[x],
self.apps_df[y],
self.apps_df['Price_Clean'],
self.apps_df['Installs_Clean'],
self.apps_df['Size_MB'],
self.apps_df['Days_Since_Update']
)
]
fig.add_trace(
go.Scatter(
x=self.apps_df[x],
y=self.apps_df[y],
mode='markers',
marker=dict(
size=4,
opacity=0.6,
color=self.apps_df['Days_Since_Update'],
colorscale=colorscale,
colorbar=dict(
title='Giorni dall\'ultimo<br>aggiornamento',
ticktext=['0', '30', '90', '180', '365', '730', '>730'],
tickvals=[0, 30, 90, 180, 365, 730, 1000]
) if idx == 1 else None,
cmin=0,
cmax=1000
),
name=f'{metrics[x]} vs {metrics[y]}',
hovertemplate="%{text}<extra></extra>",
text=hover_text,
showlegend=False
),
row=row, col=col
)
# Aggiornamento degli assi
fig.update_xaxes(title=metrics[x], row=row, col=col, gridcolor='lightgray', showgrid=True)
fig.update_yaxes(title=metrics[y], row=row, col=col, gridcolor='lightgray', showgrid=True)
fig.update_layout(
title='Scatter plots delle relazioni principali',
height=800,
width=1000,
showlegend=False,
title_x=0.5,
hovermode='closest',
plot_bgcolor='white',
margin=dict(t=100, l=50, r=50, b=50)
)
return fig
def analyze_correlations(self) -> Tuple[pd.DataFrame, pd.DataFrame, List[go.Figure]]:
metrics = {
'Rating': 'Rating',
'Price_Clean': 'Prezzo',
'Log_Installs': 'Log installazioni',
'Log_Size': 'Log dimensione',
'Days_Since_Update': 'Giorni da ultimo aggiornamento'
}
# Calcolo correlazioni
data = self.apps_df[metrics.keys()]
pearson_corr = data.corr(method='pearson').round(3)
spearman_corr = data.corr(method='spearman').round(3)
# Rinomina colonne
for corr_matrix in [pearson_corr, spearman_corr]:
corr_matrix.columns = metrics.values()
corr_matrix.index = metrics.values()
# Creazione grafici
figures = []
# Heatmap correlazioni
heatmap_fig = make_subplots(
rows=1, cols=2,
subplot_titles=('Correlazioni di Pearson', 'Correlazioni di Spearman'),
horizontal_spacing=0.15
)
# Aggiungi heatmap Pearson
heatmap_fig.add_trace(
go.Heatmap(
z=pearson_corr.values,
x=pearson_corr.columns,
y=pearson_corr.index,
colorscale='RdBu',
zmin=-1,
zmax=1,
text=pearson_corr.values.round(2),
texttemplate='%{text}',
textfont={"size": 10}
),
row=1, col=1
)
# Aggiungi heatmap Spearman
heatmap_fig.add_trace(
go.Heatmap(
z=spearman_corr.values,
x=spearman_corr.columns,
y=spearman_corr.index,
colorscale='RdBu',
zmin=-1,
zmax=1,
text=spearman_corr.values.round(2),
texttemplate='%{text}',
textfont={"size": 10}
),
row=1, col=2
)
heatmap_fig.update_layout(
title='Confronto correlazioni Pearson vs Spearman',
title_x=0.5,
height=600,
width=1500, # Aumentato a 1500
font=dict(family="Arial", size=12),
margin=dict(t=100, l=100, r=100, b=50)
)
figures.append(heatmap_fig)
# Scatter matrix
scatter_pairs = [
('Rating', 'Log_Installs'),
('Rating', 'Log_Size'),
('Log_Size', 'Log_Installs'),
('Rating', 'Days_Since_Update')
]
scatter_fig = make_subplots(
rows=2, cols=2,
subplot_titles=[
f'{metrics[x]} vs {metrics[y]}'
for x, y in scatter_pairs
],
horizontal_spacing=0.15,
vertical_spacing=0.15
)
# Creazione di una scala di colori basata sul rating
colorscale = [
[0, 'red'], # Rating 1
[0.25, 'orange'], # Rating 2
[0.5, 'yellow'], # Rating 3
[0.75, 'lightgreen'], # Rating 4
[1, 'green'] # Rating 5
]
for idx, (x, y) in enumerate(scatter_pairs):
row = idx // 2 + 1
col = idx % 2 + 1
hover_text = [
f"App: {app}<br>" +
f"Categoria: {cat}<br>" +
f"{metrics[x]}: {val_x:.2f}<br>" +
f"{metrics[y]}: {val_y:.2f}<br>" +
f"Rating: {rating:.1f}<br>" +
f"Prezzo: ${price:.2f}<br>" +
f"Installazioni: {inst:,.0f}<br>" +
f"Dimensione: {size:.1f}MB<br>" +
f"Giorni dall'ultimo aggiornamento: {days:.0f}"
for app, cat, val_x, val_y, rating, price, inst, size, days in zip(
self.apps_df['App'],
self.apps_df['Category'],
self.apps_df[x],
self.apps_df[y],
self.apps_df['Rating'],
self.apps_df['Price_Clean'],
self.apps_df['Installs_Clean'],
self.apps_df['Size_MB'],
self.apps_df['Days_Since_Update']
)
]
if y == 'Days_Since_Update':
# Aggiunge jitter al rating per evitare sovrapposizioni
jittered_x = self.apps_df[x] + np.random.normal(0, 0.05, len(self.apps_df))
# Calcola la media mobile
rating_range = np.arange(1, 5.1, 0.1)
days_mean = []
for r in rating_range:
mask = (self.apps_df[x] >= r - 0.2) & (self.apps_df[x] < r + 0.2)
mean_val = self.apps_df.loc[mask, y].mean()
days_mean.append(mean_val)
scatter_fig.add_trace(
go.Scatter(
x=jittered_x,
y=self.apps_df[y],
mode='markers',
marker=dict(
size=3,
opacity=0.3,
color=self.apps_df['Rating'],
colorscale=colorscale,
colorbar=dict(
title='Rating',
ticktext=['1', '2', '3', '4', '5'],
tickvals=[1, 2, 3, 4, 5]
) if idx == 1 else None,
cmin=1,
cmax=5
),
name=f'{metrics[x]} vs {metrics[y]}',
hovertemplate="%{text}<extra></extra>",
text=hover_text,
showlegend=False
),
row=row, col=col
)
# Aggiunge la linea della media mobile
scatter_fig.add_trace(
go.Scatter(
x=rating_range,
y=days_mean,
mode='lines',
line=dict(color='black', width=2),
name='Media mobile',
showlegend=False
),
row=row, col=col
)
# Aggiorna il layout per questo subplot specifico
scatter_fig.update_xaxes(
title=metrics[x],
row=row,
col=col,
gridcolor='lightgray',
showgrid=True,
range=[0.5, 5.5]
)
# Usa il 95° percentile per l'asse y
y_max = self.apps_df[y].quantile(0.95)
scatter_fig.update_yaxes(
title=metrics[y],
row=row,
col=col,
gridcolor='lightgray',
showgrid=True,
range=[0, y_max]
)
else:
scatter_fig.add_trace(
go.Scatter(
x=self.apps_df[x],
y=self.apps_df[y],
mode='markers',
marker=dict(
size=4,
opacity=0.6,
color=self.apps_df['Rating'],
colorscale=colorscale,
colorbar=dict(
title='Rating',
ticktext=['1', '2', '3', '4', '5'],
tickvals=[1, 2, 3, 4, 5]
) if idx == 1 else None,
cmin=1,
cmax=5
),
name=f'{metrics[x]} vs {metrics[y]}',
hovertemplate="%{text}<extra></extra>",
text=hover_text,
showlegend=False
),
row=row, col=col
)
scatter_fig.update_xaxes(
title=metrics[x],
row=row,
col=col,
gridcolor='lightgray',
showgrid=True
)
scatter_fig.update_yaxes(
title=metrics[y],
row=row,
col=col,
gridcolor='lightgray',
showgrid=True
)
scatter_fig.update_layout(
title='Scatter plots delle relazioni principali',
height=800,
width=1500, # Aumentato a 1500
showlegend=False,
title_x=0.5,
hovermode='closest',
plot_bgcolor='white',
margin=dict(t=100, l=50, r=50, b=50)
)
figures.append(scatter_fig)
return pearson_corr, spearman_corr, figures
def analyze_temporal_trends(self) -> TimeMetrics:
# Verifica preliminare prezzi
avg_price = self.apps_df.groupby('Update_Year').agg({
'Price_Clean': lambda x: x[x > 0].mean() if len(x[x > 0]) > 0 else np.nan
})
# Aggregazione metriche temporali efficiente
time_metrics = self.apps_df.groupby('Update_Year').agg({
'Rating': 'mean',
'Size_MB': 'mean',
'Installs_Clean': 'mean',
'App': 'count'
}).round(2)
time_metrics['Price_Clean'] = avg_price['Price_Clean']
# Creazione figura ottimizzata con subplot
fig = make_subplots(
rows=2,
cols=1,
row_heights=[0.6, 0.4],
vertical_spacing=0.12,
subplot_titles=(
'Metriche di prodotto (Rating, Prezzo, Dimensione)',
'Metriche di mercato (Installazioni e numero di app)'
)
)
# Configurazione tracce subplot 1
traces_subplot1 = [
('Rating', 'Rating medio', '#2ECC40'),
('Price_Clean', 'Prezzo medio ($)', '#FF4136'),
('Size_MB', 'Dimensione media (MB)', '#0074D9')
]
# Aggiunta tracce subplot 1
for col, name, color in traces_subplot1:
data = time_metrics[col].fillna(0)
hover_text = [
f"Anno: {year}<br>{name}: {self._format_trend_value(val, col.lower())}"
for year, val in zip(time_metrics.index, time_metrics[col])
]
fig.add_trace(
go.Scatter(
x=time_metrics.index,
y=data,
name=name,
line=dict(color=color, width=2),
hovertemplate="%{text}<extra></extra>",
text=hover_text
),
row=1,
col=1
)
# Aggiunta barre numero app subplot 2
hover_text_app = [
f"Anno: {year}<br>Numero di app: {self._format_trend_value(val, 'app')}"
for year, val in zip(time_metrics.index, time_metrics['App'])
]
fig.add_trace(
go.Bar(
x=time_metrics.index,
y=time_metrics['App'],
name='Numero di app',
marker_color='#AAAAAA',
opacity=0.3,
width=0.5,
hovertemplate="%{text}<extra></extra>",
text=hover_text_app
),
row=2,
col=1
)
# Aggiunta linea installazioni subplot 2
hover_text_inst = [
f"Anno: {year}<br>Installazioni medie: {self._format_trend_value(val, 'installazioni')}"
for year, val in zip(time_metrics.index, time_metrics['Installs_Clean'])
]
fig.add_trace(
go.Scatter(
x=time_metrics.index,
y=time_metrics['Installs_Clean'],
name='Installazioni medie',
line=dict(color='#FF851B', width=2),
hovertemplate="%{text}<extra></extra>",
text=hover_text_inst
),
row=2,
col=1
)
# Ottimizzazione layout
fig.update_layout(
title={
'text': 'Evoluzione temporale delle metriche chiave',
'y': 0.98,
'x': 0.5,
'xanchor': 'center',
'yanchor': 'top'
},
height=900,
showlegend=True,
legend=dict(
orientation='h',
yanchor='bottom',
y=1.05,
xanchor='center',
x=0.5,
bgcolor='rgba(255, 255, 255, 0.8)',
bordercolor='lightgray',
borderwidth=1
),
plot_bgcolor='white',
hovermode='x unified',
margin=dict(t=120, b=50, l=50, r=50)
)
# Ottimizzazione assi
for row in [1, 2]:
fig.update_xaxes(
title='Anno',
showgrid=True,
gridwidth=1,
gridcolor='lightgray',
row=row
)
fig.update_yaxes(
title='Valore',
showgrid=True,
gridwidth=1,
gridcolor='lightgray',
row=1,
col=1
)
fig.update_yaxes(
title='Numero',
showgrid=True,
gridwidth=1,
gridcolor='lightgray',
type='log',
row=2,
col=1
)
return TimeMetrics(time_metrics, fig)
def _format_trend_value(self, value: float, metric_type: str) -> str:
if pd.isna(value):
return "N/D"
if metric_type == "installazioni":
if value >= 1e6:
return f"{value/1e6:.1f}M"
elif value >= 1e3:
return f"{value/1e3:.1f}K"
return f"{value:.0f}"
elif metric_type == "dimensione":
return f"{value:.1f}MB"
elif metric_type == "prezzo":
return f"${value:.2f}"
elif metric_type == "rating":
return f"{value:.2f}"
elif metric_type == "app":
return f"{int(value):,}"
return str(value)
def analyze_play_store(apps_df: pd.DataFrame, reviews_df: Optional[pd.DataFrame] = None) -> Dict[str, Any]:
logger.info("=== ANALISI GOOGLE PLAY STORE ===")
# Inizializzazione analyzer
analyzer = PlayStoreAnalyzer(apps_df, reviews_df)
figures = []
results = {}
try:
# 1. Analisi correlazioni
logger.info("\n1. Analisi correlazioni tra metriche")
pearson_corr, spearman_corr, corr_figures = analyzer.analyze_correlations()
results['correlations'] = {'pearson': pearson_corr, 'spearman': spearman_corr}
figures.extend(corr_figures)
# Log correlazioni
for method, corr_matrix in [('Pearson', pearson_corr), ('Spearman', spearman_corr)]:
logger.info(f"\nCorrelazioni {method} significative (|corr| > 0.3):")
for i in range(len(corr_matrix.columns)):
for j in range(i+1, len(corr_matrix.columns)):
corr = corr_matrix.iloc[i, j]
if abs(corr) > 0.3:
logger.info(
f"{corr_matrix.index[i]} vs {corr_matrix.columns[j]}: {corr:.3f}"
)
# 2. Analisi temporale
logger.info("\n2. Analisi trend temporali")
time_metrics, time_fig = analyzer.analyze_temporal_trends()
results['temporal'] = time_metrics
figures.append(time_fig)
# Log trend principali
first_metrics = time_metrics.iloc[0]
last_metrics = time_metrics.iloc[-1]
rating_change = ((last_metrics['Rating'] - first_metrics['Rating']) /
first_metrics['Rating'] * 100)
logger.info("\nTrend principali:")
logger.info(
f"Rating medio: {rating_change:+.1f}% di variazione "
f"(da {first_metrics['Rating']:.2f} a {last_metrics['Rating']:.2f})"
)
logger.info(
f"Dimensione media: da {first_metrics['Size_MB']:.1f}MB a "
f"{last_metrics['Size_MB']:.1f}MB"
)
def format_installs(val):
return f"{val/1e6:.1f}M" if val >= 1e6 else f"{val/1e3:.1f}K"
logger.info(
f"Installazioni medie: da {format_installs(first_metrics['Installs_Clean'])} a "
f"{format_installs(last_metrics['Installs_Clean'])}"
)
logger.info(
f"Numero di app: da {int(first_metrics['App']):,} a "
f"{int(last_metrics['App']):,}"
)
if pd.notna(first_metrics['Price_Clean']) and pd.notna(last_metrics['Price_Clean']):
logger.info(
f"Prezzo medio: da ${first_metrics['Price_Clean']:.2f} a "
f"${last_metrics['Price_Clean']:.2f}"
)
else:
logger.info("Prezzo medio: dati non disponibili")
results['figures'] = figures
return results
except Exception as e:
logger.error(f"Errore durante l'analisi del Play Store: {str(e)}")
raise
# Esecuzione dell'analisi
analysis_results = analyze_play_store(apps_clean, reviews_clean)
# Visualizzazione dei grafici
for fig in analysis_results['figures']:
fig.show()